package com.leigq.dynamic.task.util;

import com.leigq.dynamic.task.web.config.RedisConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Service;

import java.util.Collections;
import java.util.Objects;


/**
 * Redis实现分布式锁，基于 RedisTemplate
 * <br/>
 * jedis 实现请看：<a href='https://blog.csdn.net/qq_28397259/article/details/80839072'>基于redisTemplate的redis的分布式锁正确打开方式</a>
 * <br/>
 * 分布式锁一般有三种实现方式：1. 数据库乐观锁；2. 基于Redis的分布式锁；3. 基于ZooKeeper的分布式锁。<br/>
 * 可靠性:<br/>
 * 为了确保分布式锁可用，我们至少要确保锁的实现同时满足以下四个条件：<br/>
 * <ul>
 * <li>
 * 1. 互斥性。在任意时刻，只有一个客户端能持有锁。
 * </li>
 * <li>
 * 2. 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁，也能保证后续其他客户端能加锁。
 * </li>
 * <li>
 * 3. 具有容错性。只要大部分的Redis节点正常运行，客户端就可以加锁和解锁。
 * </li>
 * <li>
 * 4. 解铃还须系铃人。加锁和解锁必须是同一个客户端，客户端自己不能把别人加的锁给解了。
 * </li>
 * </ul>
 * 参考：<br/>
 * <ul>
 * <li>
 * <a href='https://www.cnblogs.com/linjiqin/p/8003838.html'>Redis分布式锁的正确实现方式</a>
 * </li>
 * <li>
 * <a href='https://blog.csdn.net/long2010110/article/details/82911168'>springboot的RedisTemplate实现分布式锁</a>
 * </li>
 * </ul>
 * <a href='https://blog.csdn.net/weixin_38399962/article/details/82753763'>使用示例参考</a>：
 * <pre>
 * {@link @Autowired}
 * private {@link RedisLock} redisLock;
 *
 * boolean locked = redisLock.lock(lockKey, requestId, expireTime);
 * if (locked) {
 *     // 执行逻辑操作
 *     ......
 *     ......
 *     redisLock.unLock(lockKey, requestId);
 * } else {
 *     // 设置失败次数计数器, 当到达5次时, 返回失败
 *     int failCount = 1;
 *     while(failCount <= 5){
 *         // 等待100ms重试
 *         try {
 *             Thread.sleep(100l);
 *         } catch (InterruptedException e) {
 *             e.printStackTrace();
 *         }
 *         if (redisLock.lock(lockKey, requestId, expireTime)){
 *            // 执行逻辑操作
 *            ......
 *            ......
 *            redisLock.unLock(lockKey, requestId);
 *         }else{
 *             failCount ++;
 *         }
 *     }
 *     throw new RuntimeException("现在创建的人太多了, 请稍等再试");
 * }
 * </pre>
 *
 * @author ：leigq
 * @date ：2019/7/2 11:22
 */
@Slf4j
@Service
public final class RedisLock {

	private final RedisTemplate<Object, Object> redisTemp;

	/**
	 * 使用 RedisConfig 中的 redisTemp，自定义序列化 及 兼容 java8 时间
	 * @see RedisConfig#getRedisTemplate(RedisConnectionFactory)
	 */
	public RedisLock(RedisTemplate<Object, Object> redisTemp) {
		this.redisTemp = redisTemp;
	}

	/**
	 * 尝试获取分布式锁
	 *
	 * @param lockKey    锁key
	 * @param requestId  请求标识 可以使用UUID.randomUUID().toString()方法生成
	 * @param expireTime 超期时间 单位秒
	 * @return 是否获取成功
	 */
	public boolean lock(String lockKey, String requestId, int expireTime) {
		// 使用脚本，保证原子性
		RedisScript<Long> redisScript = RedisScript.of(LOCK_LUA, Long.class);
		Object lockResult = redisTemp.execute(redisScript, Collections.singletonList(lockKey), requestId, expireTime);
		log.warn("lock executeResult is [{}]", lockResult);
		return Objects.equals(SUCCESS, lockResult);
		// 不符合原子性
//		Boolean setIfAbsentResult = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, requestId);
//		Boolean setExpireResult = stringRedisTemplate.expire(lockKey, expireTime, TimeUnit.SECONDS);
//		return setIfAbsentResult && setExpireResult;
	}

	/**
	 * 释放分布式锁
	 *
	 * @param lockKey   锁key
	 * @param requestId 请求标识 可以使用UUID.randomUUID().toString()方法生成
	 * @return 是否释放成功
	 */
	public boolean unLock(String lockKey, String requestId) {
		RedisScript<Long> redisScript = RedisScript.of(UNLOCK_LUA, Long.class);
		Object unLockResult = redisTemp.execute(redisScript, Collections.singletonList(lockKey), requestId);
		log.warn("unLock executeResult is [{}]", unLockResult);
		return Objects.equals(SUCCESS, unLockResult);
	}

	private static final Long SUCCESS = 1L;


    /**
     * 加锁 Lua 脚本
     */
    private static final String LOCK_LUA;

    /**
     * 解锁 Lua 脚本
     */
    private static final String UNLOCK_LUA;


	static {
		// if redis.call('setNx', KEYS[1], ARGV[1]) then if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('expire', KEYS[1], ARGV[2]) else return 0 end end
		LOCK_LUA = "if redis.call('setNx', KEYS[1], ARGV[1]) " +
				"then " +
				"    if redis.call('get', KEYS[1]) == ARGV[1] " +
				"    then " +
				"        return redis.call('expire', KEYS[1], ARGV[2]) " +
				"    else " +
				"        return 0 " +
				"    end " +
				"end ";

		// if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end
		UNLOCK_LUA = "if redis.call('get', KEYS[1]) == ARGV[1] " +
				"then " +
				"    return redis.call('del', KEYS[1]) " +
				"else " +
				"    return 0 " +
				"end ";
	}

}
